using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Phoenix.Xna.Framework.ContentPipeline.Animation;
using Phoenix.Xna.Framework.ContentPipeline.Properties;
using Phoenix.Xna.Framework.Graphics.Animation;
using Phoenix.Xna.Framework.Graphics.Models;
using Phoenix.Xna.Framework.Xml;

namespace Phoenix.Xna.Framework.ContentPipeline.Models
{
    using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

    [ContentProcessor(DisplayName = "Skinned Model - Phoenix XNA Engine")]
    public class SkinnedModelProcessor : ContentProcessor<NodeContent, SkinnedModelContent>
    {
        public static readonly int DefaultAnimationFramerate = 60;

        [DisplayName("Bake Mesh Transforms"),
            Description("If enabled, bake the mesh transform into its geometry."),
            DefaultValue(typeof(bool), "true")]
        public virtual bool BakeMeshTransforms { get; set; }
        [DisplayName("Generate Tangent Frame"),
            Description("If enabled, generates the tangent and binormal for each mesh's vertex."),
            DefaultValue(typeof(bool), "false")]
        public virtual bool GenerateTangentFrame { get; set; }
        [DisplayName("Texture Path"),
            Description("If set, changes the path of all texture resources. The new path can be an absolute or relative path."),
            DefaultValue(typeof(string), "")]
        public virtual string TexturePath { get; set; }
        [DisplayName("Texture Path Type"),
            Description("Indicates whether the 'Texture Path' is an absolute or relative path.  If the path is relative, it is relative to the source asset path."),
            DefaultValue(typeof(PathType), "Relative")]
        public virtual PathType TexturePathType { get; set; }
        [DisplayName("Split Animation Filename"),
            Description("If set, this file will be used to split the model's animations."),
            DefaultValue(typeof(string), "")]
        public virtual string SplitAnimationFileName { get; set; }
        [DisplayName("Model Rotation"),
            Description("Rotates the model a specified number of degrees over the Y, X and Z axis in this order."),
            DefaultValue(typeof(Vector3), "0, 0, 0")]
        public virtual Vector3 ModelRotation { get; set; }
        [DisplayName("Model Scale"),
            Description("Uniformly scales the model."),
            DefaultValue(typeof(float), "1")]
        public virtual float ModelScale { get; set; }
        [DisplayName("Debug Hierarchy"),
            Description("Dump model hierarchy for debugging."),
            DefaultValue(typeof(bool), "false")]
        public virtual bool DebugHierarchy { get; set; }
        [DisplayName("Debug Animations"),
            Description("Dump model animations for debugging."),
            DefaultValue(typeof(bool), "false")]
        public virtual bool DebugAnimations { get; set; }

        protected virtual void ProcessMesh(MeshContent mesh, ContentProcessorContext context)
        {
            if (BakeMeshTransforms)
            {
                Matrix vertexTransform = mesh.AbsoluteTransform;

                for (int i = 0; i < mesh.Positions.Count; i++)
                {
                    mesh.Positions[i] = Vector3.Transform(mesh.Positions[i], vertexTransform);
                }
            }

            if (GenerateTangentFrame)
            {
                MeshHelper.CalculateTangentFrames(mesh,
                    VertexChannelNames.TextureCoordinate(0),
                    VertexChannelNames.Tangent(0),
                    VertexChannelNames.Binormal(0));
            }
        }

        protected virtual void ProcessGeometry(GeometryContent geometry, ContentProcessorContext context)
        {
            if (BakeMeshTransforms)
            {
                Matrix vertexTransform = geometry.Parent.AbsoluteTransform;
                Matrix vectorTransform = Matrix.Transpose(Matrix.Invert(vertexTransform));

                foreach (VertexChannel vertexChannel in geometry.Vertices.Channels)
                {
                    if (IsVectorChannel(vertexChannel) && vertexChannel.ElementType == typeof(Vector3))
                    {
                        VertexChannel<Vector3> vectorChannel = (VertexChannel<Vector3>)vertexChannel;

                        for (int i = 0; i < vectorChannel.Count; i++)
                        {
                            vectorChannel[i] = Vector3.Transform(vectorChannel[i], vectorTransform);
                            vectorChannel[i].Normalize();
                        }
                    }
                }
            }
        }

        protected virtual MaterialContent ProcessMaterial(MaterialContent materialContent, ContentProcessorContext context)
        {
            OpaqueDataDictionary processorParameters = new OpaqueDataDictionary();
            processorParameters.Add("TexturePath", (TexturePath == null) ? string.Empty : TexturePath);
            processorParameters.Add("TexturePathType", TexturePathType);
            return context.Convert<MaterialContent, MaterialContent>(materialContent,
                    "SkinnedModelMaterialProcessor",
                    processorParameters);
        }

        private void TransformScene(NodeContent input)
        {
            Matrix transform = Matrix.Identity;

            if (ModelRotation != Vector3.Zero)
            {
                Vector3 degreeRotation;
                degreeRotation.X = MathHelper.ToRadians(ModelRotation.X);
                degreeRotation.Y = MathHelper.ToRadians(ModelRotation.Y);
                degreeRotation.Z = MathHelper.ToRadians(ModelRotation.Z);
                transform = Matrix.CreateRotationY(degreeRotation.Y) *
                    Matrix.CreateRotationX(degreeRotation.X) *
                    Matrix.CreateRotationZ(degreeRotation.Z);
            }

            if (ModelScale != 1)
            {
                transform = Matrix.CreateScale((ModelScale <= 0) ? 1 : ModelScale) * transform;
            }

            if (transform != Matrix.Identity)
            {
                MeshHelper.TransformScene(input, transform);
            }
        }

        private void ProcessMeshes(NodeContent node, ContentProcessorContext context)
        {
            MeshContent mesh = node as MeshContent;

            if (mesh != null)
            {
                if (!ValidateMesh(mesh, context))
                {
                    return;
                }

                ProcessMesh(mesh, context);

                foreach (GeometryContent geometry in mesh.Geometry)
                {
                    ProcessGeometry(geometry, context);
                }
            }

            foreach (NodeContent child in node.Children)
            {
                ProcessMeshes(child, context);
            }
        }

        private void ProcessMaterials(NodeContent node, ContentProcessorContext context)
        {
            Dictionary<MaterialContent, MaterialContent> processedMaterials =
                new Dictionary<MaterialContent, MaterialContent>();
            ProcessMaterials(node, processedMaterials, context);
        }

        private void ProcessMaterials(NodeContent node, Dictionary<MaterialContent,
            MaterialContent> processedMaterials, ContentProcessorContext context)
        {
            MeshContent mesh = node as MeshContent;

            if (mesh != null)
            {
                foreach (GeometryContent geometry in mesh.Geometry)
                {
                    if (!ValidateGeometry(geometry, context))
                    {
                        continue;
                    }

                    MaterialContent processedMaterial;

                    if (!processedMaterials.TryGetValue(geometry.Material, out processedMaterial))
                    {
                        processedMaterial = ProcessMaterial(geometry.Material, context);
                        processedMaterials.Add(geometry.Material, processedMaterial);
                    }

                    geometry.Material = processedMaterial;
                }
            }

            foreach (NodeContent child in node.Children)
            {
                ProcessMaterials(child, processedMaterials, context);
            }
        }

        private SkinnedModelBoneContentCollection ProcessBones(BoneContent rootBone, ContentProcessorContext context)
        {
            List<SkinnedModelBoneContent> skinnedBoneList = new List<SkinnedModelBoneContent>(SkinnedModelBasicEffect.MaxSupportedBones);
            ProcessBones(rootBone, null, skinnedBoneList, context);
            return new SkinnedModelBoneContentCollection(skinnedBoneList);
        }

        private SkinnedModelBoneContent ProcessBones(BoneContent boneContent, SkinnedModelBoneContent skinnedModelParentBone,
            List<SkinnedModelBoneContent> skinnedBoneList, ContentProcessorContext context)
        {
            ushort boneIndex = (ushort)skinnedBoneList.Count;
            Pose bindPose;
            boneContent.Transform.Decompose(out bindPose.Scale, out bindPose.Orientation, out bindPose.Translation);
            Matrix inverseBindPose = Matrix.Invert(boneContent.AbsoluteTransform);
            SkinnedModelBoneContent skinnedModelBone = new SkinnedModelBoneContent(boneIndex,
                boneContent.Name,
                bindPose,
                inverseBindPose);
            skinnedBoneList.Add(skinnedModelBone);
            List<SkinnedModelBoneContent> skinnedBoneChildrenList = new List<SkinnedModelBoneContent>();

            foreach (NodeContent nodeContent in boneContent.Children)
            {
                if (!ValidateBone(nodeContent, context))
                {
                    continue;
                }

                BoneContent childBoneContent = nodeContent as BoneContent;
                SkinnedModelBoneContent skinnedBoneChild = ProcessBones(childBoneContent,
                    skinnedModelBone,
                    skinnedBoneList,
                    context);
                skinnedBoneChildrenList.Add(skinnedBoneChild);
            }

            skinnedModelBone.Parent = skinnedModelParentBone;
            skinnedModelBone.Children = new SkinnedModelBoneContentCollection(skinnedBoneChildrenList);
            return skinnedModelBone;
        }

        private AnimationClipContentDictionary ProcessAnimations(NodeContent input, AnimationContentDictionary animationDictionary,
            SkinnedModelBoneContentCollection boneCollection, ContentProcessorContext context)
        {
            Dictionary<string, AnimationClipContent> animationClipDictionary = new Dictionary<string, AnimationClipContent>();

            foreach (AnimationContent animation in animationDictionary.Values)
            {
                if (!ValidateAnimation(animation, context))
                {
                    continue;
                }

                Dictionary<string, AnimationChannelContent> animationChannelDictionary = new Dictionary<string, AnimationChannelContent>();

                foreach (KeyValuePair<string, AnimationChannel> animationChannelPair in animation.Channels)
                {
                    if (!ValidateAnimationChannel(animationChannelPair, animation, boneCollection, context))
                    {
                        continue;
                    }

                    List<AnimationKeyframeContent> keyframeList = new List<AnimationKeyframeContent>(animationChannelPair.Value.Count);

                    foreach (AnimationKeyframe channelKeyframe in animationChannelPair.Value)
                    {
                        Pose keyframePose;
                        channelKeyframe.Transform.Decompose(out keyframePose.Scale,
                            out keyframePose.Orientation,
                            out keyframePose.Translation);
                        keyframeList.Add(new AnimationKeyframeContent(channelKeyframe.Time, keyframePose));
                    }

                    keyframeList.Sort();
                    animationChannelDictionary.Add(animationChannelPair.Key, new AnimationChannelContent(keyframeList));
                }

                AnimationClipContent animationClip = new AnimationClipContent(animation.Name,
                    new AnimationChannelContentDictionary(animationChannelDictionary),
                    animation.Duration);
                animationClipDictionary.Add(animation.Name, animationClip);
            }

            if (!string.IsNullOrEmpty(SplitAnimationFileName))
            {
                SplitAnimations(input, animationClipDictionary, context);
            }

            return new AnimationClipContentDictionary(animationClipDictionary);
        }

        private List<SplitTask> ParseTaskList(XElement element, float framerate,
            TimeSpan duration, ContentProcessorContext context)
        {
            List<SplitTask> splitAnimations = new List<SplitTask>();
            List<string> splitAnimationNames = new List<string>();
            string id = element.ReadAttribute<string>("ID");
            SplitTaskParameters parameters = new SplitTaskParameters()
            {
                Framerate = framerate,
                Duration = duration,
                AnimationID = id
            };

            foreach (XNode node in element.Nodes())
            {
                if (node.NodeType == XmlNodeType.Element)
                {
                    XElement e = node as XElement;

                    if (e != null)
                    {
                        if (e != null && string.Compare(e.Name.ToString(), "splittask", true) == 0)
                        {
                            SplitTask task = new SplitTask();
                            task.LoadXElement<SplitTaskParameters>(e, parameters);

                            if (splitAnimationNames.Contains(task.ID))
                            {
                                throw new InvalidContentException(string.Format(Resources.DuplicateAnimationNameException,
                                    id,
                                    task.ID));
                            }

                            splitAnimationNames.Add(task.ID);
                            splitAnimations.Add(task);
                        }
                    }
                }
            }

            return splitAnimations;
        }

        private void SplitAnimations(NodeContent input, IDictionary<string, AnimationClipContent> animations,
            ContentProcessorContext context)
        {
            string sourceAssetPath = Path.GetDirectoryName(input.Identity.SourceFilename);
            string fullFilePath = Path.GetFullPath(Path.Combine(sourceAssetPath, SplitAnimationFileName));
            XElement element = XElement.Load(fullFilePath);
            bool hasAnimations = false;

            if (element == null || string.Compare(element.Name.ToString(), "animations", true) != 0)
            {
                context.Logger.LogWarning(null, null, Resources.NoAnimationsWarning);
                return;
            }

            foreach (XNode node in element.Nodes())
            {
                if (node.NodeType == XmlNodeType.Element)
                {
                    XElement e = node as XElement;

                    if (e != null)
                    {
                        if (string.Compare(e.Name.ToString(), "animation", true) == 0)
                        {
                            hasAnimations = true;
                            string id = e.ReadAttribute<string>("ID");
                            float framerate = 0;
                            AnimationClipContent animationClip;

                            if (string.IsNullOrEmpty(id))
                            {
                                throw new InvalidContentException(Resources.AnimationIDException);
                            }

                            try
                            {
                                framerate = e.ReadAttribute<float>("Framerate");
                            }
                            catch
                            {
                                framerate = DefaultAnimationFramerate;
                            }

                            if (animations.TryGetValue(id, out animationClip))
                            {
                                List<SplitTask> splitAnimationTaskList = ParseTaskList(e,
                                    framerate,
                                    animationClip.Duration,
                                    context);
                                List<KeyValuePair<string, AnimationClipContent>> keySplitAnimationClips = SplitAnimation(
                                    animationClip,
                                    splitAnimationTaskList,
                                    context);

                                foreach (KeyValuePair<string, AnimationClipContent> keySplitAnimationClip in keySplitAnimationClips)
                                {
                                    animations.Add(keySplitAnimationClip);
                                }

                            }
                            else
                            {
                                throw new InvalidContentException(string.Format(Resources.AnimationNullException, id));
                            }
                        }
                    }
                }
            }

            if (!hasAnimations)
            {
                context.Logger.LogWarning(null, null, Resources.NoAnimationsWarning);
            }
        }

        private List<KeyValuePair<string, AnimationClipContent>> SplitAnimation(AnimationClipContent clip,
            List<SplitTask> taskList, ContentProcessorContext context)
        {
            List<KeyValuePair<string, AnimationClipContent>> keyAnimationClipList = new List<KeyValuePair<string, AnimationClipContent>>(taskList.Count);

            foreach (SplitTask t in taskList)
            {
                Dictionary<string, AnimationChannelContent> animations = new Dictionary<string, AnimationChannelContent>();
                TimeSpan splitAnimationDuration = TimeSpan.Zero;

                foreach (KeyValuePair<string, AnimationChannelContent> p in clip.Channels)
                {
                    List<AnimationKeyframeContent> frames = new List<AnimationKeyframeContent>();
                    TimeSpan? channelStartTime = null;

                    foreach (AnimationKeyframeContent keyframe in p.Value)
                    {
                        if (keyframe.Time.TotalSeconds >= t.StartTime && keyframe.Time.TotalSeconds <= t.EndTime)
                        {
                            if (channelStartTime == null)
                            {
                                channelStartTime = keyframe.Time;
                            }

                            TimeSpan newKeyframeTime = keyframe.Time - channelStartTime.Value;
                            frames.Add(new AnimationKeyframeContent(newKeyframeTime, keyframe.Pose));

                            if (newKeyframeTime > splitAnimationDuration)
                            {
                                splitAnimationDuration = newKeyframeTime;
                            }
                        }
                    }

                    if (frames.Count > 0)
                    {
                        animations.Add(p.Key, new AnimationChannelContent(frames));
                    }
                }

                AnimationClipContent splitAnimationClip = new AnimationClipContent(t.ID,
                    new AnimationChannelContentDictionary(animations),
                    splitAnimationDuration);
                keyAnimationClipList.Add(new KeyValuePair<string, AnimationClipContent>(t.ID, splitAnimationClip));
            }

            return keyAnimationClipList;
        }

        private bool IsVectorChannel(VertexChannel vertexChannel)
        {
            return (vertexChannel.Name.StartsWith("Normal") ||
                vertexChannel.Name.StartsWith("Tangent") ||
                vertexChannel.Name.StartsWith("Binormal"));
        }

        private bool ValidateModel(NodeContent input, BoneContent rootBone, ContentProcessorContext context)
        {
            if (rootBone == null)
            {
                throw new InvalidContentException(Resources.SkeletonNullException);
            }

            IList<BoneContent> boneList = MeshHelper.FlattenSkeleton(rootBone);

            if (boneList.Count > SkinnedModelBasicEffect.MaxSupportedBones)
            {
                throw new InvalidContentException(string.Format(Resources.MaxBonesException,
                    boneList.Count,
                    SkinnedModelBasicEffect.MaxSupportedBones));
            }

            AnimationContentDictionary animationDictionary = rootBone.Animations;

            if (animationDictionary.Count == 0)
            {
                context.Logger.LogWarning(null, rootBone.Identity, Resources.NoAnimationsWarning);
            }

            return true;
        }

        private bool ValidateMesh(MeshContent mesh, ContentProcessorContext context)
        {
            if (mesh.Parent is BoneContent)
            {
                context.Logger.LogWarning(null, mesh.Identity, Resources.ChildBoneWarning, mesh.Name, mesh.Parent.Name);
            }

            return true;
        }

        private bool ValidateGeometry(GeometryContent geometry, ContentProcessorContext context)
        {
            if (geometry.Material == null)
            {
                throw new InvalidContentException(string.Format(Resources.MaterialNullException, geometry.Parent.Name));
            }

            if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))
            {
                throw new InvalidContentException(string.Format(Resources.BlendWeightsChannelNullException,
                    geometry.Parent.Name));
            }

            return true;
        }

        private bool ValidateBone(NodeContent nodeContent, ContentProcessorContext context)
        {
            BoneContent boneContent = nodeContent as BoneContent;

            if (boneContent == null)
            {
                context.Logger.LogWarning(null,
                    nodeContent.Identity,
                    string.Format(Resources.InvalidNodeWarning, nodeContent.Name));
                return false;
            }

            return true;
        }

        private bool ValidateAnimation(AnimationContent animation, ContentProcessorContext context)
        {
            if (animation.Channels.Count == 0)
            {
                context.Logger.LogWarning(null,
                    animation.Identity,
                    String.Format(Resources.NoChannelsWarning, animation.Name));
                return false;
            }

            if (animation.Duration <= TimeSpan.Zero)
            {
                context.Logger.LogWarning(null,
                    animation.Identity,
                    String.Format(Resources.ZeroDurationWarning, animation.Name));
                return false;
            }

            return true;
        }

        private bool ValidateAnimationChannel(KeyValuePair<string, AnimationChannel> animationChannelPair,
            AnimationContent parentAnimation, SkinnedModelBoneContentCollection boneCollection,
            ContentProcessorContext context)
        {
            if (animationChannelPair.Value.Count == 0)
            {
                context.Logger.LogWarning(null,
                    parentAnimation.Identity,
                    String.Format(Resources.NoKeyFramesWarning, animationChannelPair.Key, parentAnimation.Name));
                return false;
            }

            bool boneFound = false;

            foreach (SkinnedModelBoneContent boneContent in boneCollection)
            {
                if (boneContent.Name.Equals(animationChannelPair.Key))
                {
                    boneFound = true;
                    break;
                }
            }

            if (!boneFound)
            {
                context.Logger.LogWarning(null,
                    parentAnimation.Identity,
                    String.Format(Resources.NullBoneWarning, animationChannelPair.Key, parentAnimation.Name));
                return false;
            }

            return true;
        }

        private void OutputHierarchyInfo(NodeContent input, ContentProcessorContext context)
        {
            context.Logger.LogImportantMessage("Dumping Hierarchy:");
            OutputHierarchyInfo(input, " ", context);
            context.Logger.LogImportantMessage("");
        }

        private void OutputHierarchyInfo(NodeContent input, string pad, ContentProcessorContext context)
        {
            string[] typeName = input.ToString().Split(new char[] { '.' });
            context.Logger.LogImportantMessage(pad + input.Name + " : " + typeName[typeName.Length - 1]);

            foreach (NodeContent child in input.Children)
            {
                OutputHierarchyInfo(child, "--" + pad, context);
            }
        }

        private void OutputAnimationInfo(NodeContent input, ContentProcessorContext context)
        {
            context.Logger.LogImportantMessage("Dumping Animations:");
            BoneContent rootBone = MeshHelper.FindSkeleton(input);
            AnimationContentDictionary animationDictionary = rootBone.Animations;

            if (animationDictionary.Count == 0)
            {
                context.Logger.LogImportantMessage(Resources.NoAnimationsDumpWarning);
            }

            foreach (AnimationContent animation in animationDictionary.Values)
            {
                context.Logger.LogImportantMessage(string.Format(Resources.AnimationDebugDump,
                    animation.Name,
                    animation.Channels.Count,
                    animation.Duration.TotalSeconds));

                foreach (KeyValuePair<string, AnimationChannel> animationChannel in animation.Channels)
                {
                    AnimationKeyframe lastKeyframe = animationChannel.Value[animationChannel.Value.Count - 1];
                    StringBuilder keyframeTimes = new StringBuilder();

                    if (animationChannel.Value.Count > 0)
                    {
                        keyframeTimes.Append(string.Format("{0:F4}", animationChannel.Value[0].Time.TotalSeconds));

                        for (int i = 1; i < animationChannel.Value.Count; i++)
                        {
                            keyframeTimes.Append(", " + string.Format("{0:F4}", animationChannel.Value[i].Time.TotalSeconds));
                        }

                        context.Logger.LogImportantMessage(string.Format(Resources.ChannelDebugDump,
                            animationChannel.Key,
                            animationChannel.Value.Count,
                            lastKeyframe.Time.TotalSeconds,
                            keyframeTimes));
                    }
                }

                context.Logger.LogImportantMessage("");
            }

            if (animationDictionary.Values.Count <= 0)
            {
                context.Logger.LogImportantMessage("");
            }
        }

        #region ContentProcessor<NodeContent, SkinnedModelContent> Members

        public override SkinnedModelContent Process(NodeContent input, ContentProcessorContext context)
        {
            if (input == null)
            {
                throw new ArgumentNullException(string.Format(Resources.ArgumentNullException, "input"));
            }

            if (DebugHierarchy)
            {
                OutputHierarchyInfo(input, context);
            }

            if (DebugAnimations)
            {
                OutputAnimationInfo(input, context);
            }

            BoneContent rootBone = MeshHelper.FindSkeleton(input);
            ValidateModel(input, rootBone, context);
            TransformScene(input);
            ProcessMeshes(input, context);
            ProcessMaterials(input, context);
            SkinnedModelBoneContentCollection skinnedModelBoneCollection = ProcessBones(rootBone, context);
            AnimationClipContentDictionary animationClipDictionary = ProcessAnimations(input,
                rootBone.Animations,
                skinnedModelBoneCollection,
                context);
            ModelContent modelContent = context.Convert<NodeContent, ModelContent>(input, "ModelProcessor");
            return new SkinnedModelContent(modelContent, skinnedModelBoneCollection, animationClipDictionary);
        }

        #endregion
    }
}